"""
本文件封装地图数据，天气数据的本地缓存管理。
"""
import datetime
import json
import os.path
import random
from navieng.grid_reader import GridReader
import numpy as np
from navieng.weather_reader import WeatherReader
import time

def rects_of_min_distance(p1, p2):
    # 给定两个点，在360度制的世界地图中，画出一个区域（不跨越国际日期变更线）或者两个区域（跨越国际日期变更线）
    # p1(x1, y1), p2(x2, y2) 经纬度。
    # y是不需要处理的。
    (x1, y1) = p1
    (x2, y2) = p2
    # 在地球上任意两个不在国际日期变更线上的点，都有两条线路，穿过日期变更线或者不穿过日期变更线。
    # A：假定在传参时x1 <= x2。（重要！下面所有描述都基于此）
    # 已知x的范围是0~360，x=0的线是国际日期变更线，x=180的线是本初子午线，
    # 不穿越国际日期变更线时的x距离=(x2-x1)，穿越国际日期变更线时的x距离=(x1-x2+360)。（P.S x1到y轴的距离是x1, x2到x=360的坐标轴边缘距离是360-x2,二者相加）
    # 判断'穿越国际日期变更线时的'x距离是否大于'不穿越国际日期变更线时的'x距离：
    # (x1-x2+360) > (x2-x1) => [2 * x1 - 2 * x2 + 360 > 0] => [x1 - x2 > -180] => [x2 - x1 < 180]
    # B：如果在传参时候x2 <= x1，则为[x1 - x2 < 180]
    # 所以当abs(x1 - x2) <= 180时穿越国际日期变更线时的线路更长。据此拓展，因为360度是在-180/+180度制加上180，二个360度制的x值和两个
    # 180/-180制的x值相减后没有差别，所以输入参数既可以是-180/+180度制也可以是360度制。
    if abs(x1 - x2) <= 180:
        print("走不穿越国际日期变更线时的线路距离更短。")
        # 矩形区域的左下坐标
        left, bottom = min(x1, x2), min(y1, y2)  # right, top = max(x1, x2), max(y1, y2)
        width, height = abs(x1 - x2), abs(y1 - y2)
        return [(left, bottom, width, height)]
    else:
        print("走穿越国际日期变更线时的线路距离更短。")
        # 需要拆成两块: 在日期变更线东边的和西边的。两个矩形具有相同的top和bottom
        top, bottom = max(y1, y2), min(y1, y2)
        height = abs(y1 - y2)
        # 东边的，left是国际日期变更线。
        e_left, e_right = 0, min(x1, x2)  # e for east
        e_width = abs(e_right - e_left)
        # 西边的，right是国际日期变更线。
        w_left, w_right = max(x1, x2), 360
        w_width = abs(w_right - w_left)
        return [(e_left, bottom, e_width, height), (w_left, bottom, w_width, height)]


class MemCacheBase:
    """
     width, height的单位统一是0.01度，即100表示1度。
    """
    def __init__(self, lon, lat, width, height, grids_per_degree):
        self.lon = lon
        self.lat = lat
        self.width = width
        self.height = height
        self.matrix: np.ndarray = np.array([])
        # 每度对应的元素数量(100 for map, 4 for weather)，给定一个"度数"的距离，乘以它的值就得到了矩阵元素的距离。
        self.grids_per_degree = grids_per_degree

    def load(self, file_path) -> (int, str):
        """
        从一个文件中载入缓存数据，载入的缓存数据的两个维度的大小必须严格等于self.width乘以self.height对应的网格数量。
        在天气matrix内部，一个元素表示0.25度的范围，在地图matrix内部，一个元素表示0.01度。
        """
        self.matrix = np.load(file_path)
        rows, cols = self.matrix.shape
        units = round(100/grids_per_degree)  # 1 for map, 25 for weather
        if rows != self.height // units or cols != self.width // units:
            self.matrix = None
            return 1, "缓存文件中的行数列数和输入参数指定的长宽不匹配。"
        else:
            return 0, "ok"

    def is_in_cache_area(self, lon_input, lat_input, h_cc, v_cc):
        """
        这个接口用于判断输入的区域是否在缓存区域内部。如果不在缓存内部就不能返回缓存的值。
        """
        assert h_cc > 0 and v_cc > 0
        # 纬度比较简单，只要lat, lat+v_cc/100的范围都在[self.lat, self.lat+self.height/100]之间就行。
        if not (self.lat <= lat_input <= self.lat + round(self.height / 100, 2)
                and self.lat <= round(lat_input + v_cc / 100, 2) <= round(self.lat + self.height / 100, 2)):
            return False, "输入纬度范围不在缓存范围内"

        # lon的判断很麻烦，因为180度线和-180度线是同一条，所以180后继续加就会变成-179.9999。
        # 1) 如果缓存区域没有跨日期线，那么它是一个连续区域。
        if round(self.lon + self.width / 100, 2) <= 180.0:
            if round(lon_input + h_cc / 100, 2) > 180.0:  # 1.1 输入区域跨越了日期线，肯定不在缓存区域内
                return False, "输入经度范围不在缓存范围内"
            else:  # 1.2 输入区域未跨越日期线，在坐标系上是一根线。判断两端是否都在区域内即可。
                if not (self.lon <= lon_input <= round(self.lon + self.width / 100, 2)
                        and self.lon <= round(lon_input + h_cc / 100, 2) <= round(self.lon + self.width / 100, 2)):
                    return False, "输入经度范围不在缓存范围内"
        else:  # 2) 如果缓存区域跨越了日期线,它会被国际日期变更线切成两个部分(x起点到180, -180到x终点)。
            l1_b, l1_e = self.lon, 180.0
            l2_b, l2_e = -180.0, round(self.lon + self.width / 100, 2) - 360
            if lon_input + h_cc / 100 > 180:  # 2.1 如输入区域也跨区域，那么输入区域的起点大于l1_b，且终点小于l2_e即可
                if not (lon_input > l1_b and round(lon_input + h_cc / 100 - 360) < l2_e):
                    return False, "输入经度范围不在缓存范围内"
            else:  # 2.2 如输入区域未跨区域，那么在[l1_b, l1_e],[l2_b, l2_e]区间之一的内部就可以。
                if not (l1_b <= round(lon_input + h_cc / 100, 2) <= l1_e
                        or l2_b <= round(lon_input + h_cc / 100, 2) <= l2_e):
                    return False, "输入经度范围不在缓存范围内"
        return True, "ok"

    def get(self, lon_input, lat_input, h_cc, v_cc):
        """
        取范围内的数据，lon_input, lat_input是地图上矩形区域的左下角起点。
        注意：虽然在360度制的坐标系里跨越了国际日期变更线的区域一个在右侧边一个在左侧边，但是无论是否跨越了国际日期变更线，我们都是只
        存储一个matrix，而且这个矩阵和地球地理空间的连续性是一致的。例如经度179到-179在地理空间上是连续的3度，那么在matrix里也是连续的
        3*grids_per_degree个单位。(300 for map or 12 for weather)
        lon_input, lat_input是地理经纬度坐标，如119.1, 22.4
        h_cc, v_cc是矩形区域的x方向上的单位数(0.01经度为一个单位)、y方向上的单位数(0.01纬度为一个单位)。
        """
        is_in, desc = self.is_in_cache_area(lon_input, lat_input, h_cc, v_cc)
        if not is_in:
            return None, desc

        gpd = self.grids_per_degree # 给定"度数"的距离，将它乘以每度对应的元素数量(100 for map,4 for weather)，就得到了元素的距离。
        # 缓存矩阵在写入文件时就已经处理好了跨日期变更线，所以找到左下角的网格序号(i,j)然后直接返回就可以了。
        # 纬度好处理。
        j = int(round(lat_input - self.lat, 2) * gpd)

        # 经度相对麻烦，先处理简单的、缓存自身没有跨国际日期变更线的情况(见test/png/cache_not_cross_idcl.png)
        if round(self.lon + self.width/100, 2) <= 180.0:
            # 前面的is_in_cache_area已经确认了输入参数在缓存范围内，因此这里不用再次判断输入参数是否跨过了日期变更线，
            # 如果跨了前面就已经return。
            i = int(round(lon_input - self.lon, 2) * gpd)
        else:
            # 再处理缓存自身跨过了国际日期变更线的情况
            if lon_input < 0:
                # 如果输入的lon是-180~0之间，那么起点索引就是坐标系上右侧块的宽度cx加上左侧块lon到-180垂线的距离，
                # 即等于cx + (lon-(-180)) = cx + lon + 180（见test/png/cache_cross_idcl_1.png）
                cx = round(180.0 - self.lon, 2) * gpd
                i = int(cx + round(lon_input + 180.0) * gpd)
            else:
                # 如果输入的lon是0~180之间，那么起点索引就是lon-self.lon.这是因为缓存矩阵在写入文件时就已经处理好了跨日期变更线，
                # 它已经是连续的了，即在缓存矩阵中，假设180是第m列，那么-179.75=>m+1,-179.50=>m+2,-179.25=>m+3 ....
                # 见(test/png/cache_idcl_2.png)
                i = int(round(lon_input - self.lon, 2) * gpd)
        ix, jx = round(h_cc * gpd / 100), round(v_cc * gpd / 100)
        return self.matrix[i : i + ix, j : j + jx]

class MapMemCache(MemCacheBase):
    """
     width, height的单位统一是0.01度，即100表示1度。
    """
    def __init__(self, lon, lat, width, height):
        MemCacheBase.__init__(self, lon, lat, width, height, 100)


class WeatherMemCache(MemCacheBase):
    """
     width, height的单位统一是0.01度，即100表示1度。
    """
    def __init__(self, lon, lat, width, height):
        MemCacheBase.__init__(self, lon, lat, width, height, 4)



class LocalCache:
    """
    server_setup_map_cache：服务器启动时候调用检查本地文件缓存是否失效。如不存在或者已经过期就取hbase读取地理信息和天气信息，然后缓存数据
    到本地文件中。
    read_map_cache：从内存缓存中读取指定区域的数据。
    read_weather_cache：从内存缓存中读取指定区域的数据。
    为简化业务逻辑，在这个类中不开放直接操作缓存文件的接口。
    TODO 缓存区域要考虑经纬度拓展的问题，在寻找A点到B点的路线时为防止AB斜角矩形区域内没有路线（例如台岛东西海岸），需要合理地对X,Y进行拓展。
    TODO 例如缓存服务区域是20度乘以20度，那么实际缓存的区域应该是20+m, 20+n，否则边缘区域可能无法寻路。see test/png/expand_lon_lat.png
    """
    def __init__(self, config_file):
        if not os.path.exists(config_file):
            raise Exception("因配置文件不存在，缓存组件无法工作")
        self.config_file = config_file
        with open(config_file) as f:
            self.config = json.load(f)
        f.close()
        self.__mem_cache_map = MapMemCache(0, 0, 0, 0)
        self.__mem_cache_weather = WeatherMemCache(0, 0, 0, 0)
        # TODO 在这里补充一些配置项的检查...e.g 对lon,lat,width,height的对齐检查。

    @staticmethod
    def __next_id(current_file_name):
        """
        这个函数的作用是取下一个缓存文件的名字，例如上一个是/var/cache/navieng/map0001.npy,则下一个是map0002.npy, map0003.npy ...
        注意为简单起见缓存文件总是以.npy结尾，这是Numpy将矩阵数据写入磁盘的标准格式。
        current_file_name: 当前缓存文件的全路径
        返回值：下一个缓存文件的全路径。
        e.g assert LocalCache.__next_id('/var/cache/navieng/map0001.npy') == '/var/cache/navieng/map0002.npy'
        """
        if current_file_name == "":
            raise Exception("配置文件路径不能为空。")
        n = os.path.normpath(current_file_name)
        dn, fn = os.path.dirname(n), os.path.basename(n)
        # 因为用numpy.save保存了缓存，所以缓存文件总应以npy结尾。
        if not fn.endswith(".npy"):
            number_suffix = fn[-4:]
            if not number_suffix.isnumeric():  # 不是类似0001的数据。
                fn += "0001"
            else:
                num = int(number_suffix) + 1
                fn = fn[0:-4] + "{:0>4d}".format(num)
            fn += ".npy"
            return os.path.join(dn, fn)
        else:
            number_suffix = fn[-8:-4]
            if not number_suffix.isnumeric():  # 不是类似0001的数据。
                fn = fn[0:-4] + "0001"
            else:
                num = int(number_suffix) + 1
                fn = fn[0:-8] + "{:0>4d}".format(num)
            fn += ".npy"
            return os.path.join(dn, fn)

    @staticmethod
    def __query_map_sync_tag() -> str:
        # 模拟ETL服务器返回一个像版本号一样的东西。此ETL接口的返回值待商定。
        return str(random.randint(0, 100000))

    @staticmethod
    def __query_weather_sync_tag() -> str:
        # 模拟ETL服务器返回一个像天气版本号一样的东西。
        return str(random.randint(0, 100000))

    def __new_map_cache(self):
        """
        创建新的cache，成功后原cache文件将废弃不用。用于：
        (1)当服务器启动时没有发现缓存文件的场景。
        (2)定时向ETL服务器查询是否有更新，如果有更新。
        # TODO 补充错误处理逻辑。
        """
        sync_tag = LocalCache.__query_map_sync_tag()  # 向ETL询问最新的同步标志（类似版本号一样的标志字符串）
        if sync_tag == self.config["cache"]["map_sync_flag"]:
            print("本地缓存的版本和ETL服务器一致，无需更新")
            return 0
        else:
            # 获取本机服务范围
            if self.config["cache"]["cache_all_map"]:
                return 1, "因全球地图数据缺失，尚不支持缓存全球地图地理数据"
            #
            # TODO 重要！要在下面补充对跨越180/-180经度线的处理。例如-179度到179是两度的范围，而不是358度的范围，我们目前的方案应是拼接
            # 两个被-180/180线隔开的地图区域。
            #
            lon, lat = self.config["cache"]["lon_begin"], self.config["cache"]["lat_begin"]  # 起始经度,起始纬度
            width, height = self.config["cache"]["width"], self.config["cache"]["height"] # 区域宽度，区域高度
            g = GridReader(self.config["datasource"]["map"]["host"], self.config["datasource"]["map"]["port"],
                           self.config["read_map_max_threads"])
            print(datetime.datetime.now(), "正在从hbase中读取地图数据...")
            map_matrix = g.read_grid_data(lon, lat, width, height)  # TODO 将这个map_matrix直接给self.map_mem_cache
            print(datetime.datetime.now(), "hbase读取地图数据完毕！")
            print(map_matrix.shape, map_matrix.dtype)

            local_file_path = LocalCache.__next_id(self.config["cache"]["map_file"])
            np.save(local_file_path, map_matrix)

            self.config["cache"]["map_file"] = local_file_path
            self.config["cache"]["map_sync_flag"] = sync_tag
            with open(self.config_file, "w") as f:
                json.dump(self.config, f, indent=4)
            f.close()
            return 0, "ok"

    def server_setup_map_cache(self):
        """
        服务器启动时/启动前调用这个。检查缓存文件是否存在。如果不存在就创建它，否则跳过创建缓存的过程开始服务。
        这样做的原因：**一是因为地图数据通常变化不大，二是地图数据量较大更新起来比较慢，所以暂定一边服务一边更新缓存。**
        【这个未来也可能改成在启动时先检查一次，具体要看后面产品的要求】
        TODO 改为异步调用，做错误处理等。
        """
        map_cache_file = self.config["cache"]["map_file"]
        if os.path.exists(map_cache_file):
            print("地图cache文件", map_cache_file, "存在。跳过地图数据缓存初始化")
        else:
            print("地图cache文件", map_cache_file, "不存在。准备执行地图数据缓存初始化")
            (ec, desc) = self.__new_map_cache()
            if ec != 0:
                return ec, desc
        (ec, desc) = self.__mem_cache_map.load(map_cache_file)  # TODO 在正式代码中，将在异步调用结束时调用它
        if ec != 0:
            return ec, desc

        weather_cache_file = self.config["cache"]["weather_file"]
        if not os.path.exists(weather_cache_file):
            print("天气cache文件", weather_cache_file, "不存在。准备执行天气数据缓存初始化")
            (ec, desc) = self.__new_weather_cache(LocalCache.__query_weather_sync_tag())
            if ec != 0:
                return ec, desc
        else:
            new_sync_tag = LocalCache.__query_weather_sync_tag()
            if self.config["cache"]["weather_sync_flag"] != new_sync_tag:
                print("天气cache文件", weather_cache_file, "已经过时，需要更新。")
                (ec, desc) = self.__new_weather_cache(new_sync_tag)
                if ec != 0:
                    return ec, desc
            else:
                print("天气cache文件不需要更新")
        (ec, desc) = self.__mem_cache_weather.load(weather_cache_file)  # TODO 在正式代码中，将在异步调用结束时调用它
        if ec != 0:
            return ec, desc
        return 0, "ok"

    def timer_proc_4_update_cache(self):
        """
        这是一个定时检查缓存数据是否过期的函数，这个处理函数应该由web程序调用。
        """
        pass

    def __new_weather_cache(self, new_sync_tag):
        # TODO 补充错误处理的代码。
        if self.config["cache"]["cache_all_weather"]:
            print("全量缓存天气数据到本地文件")
            lon, lat, width, height = 0, 0, 36000, 18000
        else:
            lon, lat = self.config["cache"]["lon_begin"], self.config["cache"]["lat_begin"]  # 起始经度,起始纬度
            width, height = self.config["cache"]["width"], self.config["cache"]["height"] # 区域宽度，区域高度

        w = WeatherReader(self.config["weather_config_file"],
                          self.config["datasource"]["weather"]["host"],
                          self.config["datasource"]["weather"]["port"])
        start_time = time.perf_counter()
        w.set_hbase_table(self.config["datasource"]["weather"]["table"])
        w.dump_config()  # 测试，打印关键的配置信息。
        next_cache_name = LocalCache.__next_id(self.config["cache"]["weather_file"])
        # TODO 读取全部数据时候真的非常非常慢，需要半个小时读完。
        # TODO 现在读取天气的接口没有办法处理跨国际日期分界线的数据，后续完善。
        w.read_weather_and_cache(lon, lat, width, height, next_cache_name)
        end_time = time.perf_counter()
        print("读取天气数据完成，总耗时:", round(end_time - start_time, 3), "秒")
        print("天气数据已缓存到本地文件:", next_cache_name)
        self.config["cache"]["weather_file"] = next_cache_name
        self.config["cache"]["weather_sync_flag"] = new_sync_tag
        with open(self.config_file, "w") as f:
            json.dump(self.config, f, indent=4)
        f.close()
        return 0, "ok"

    def get_map_data(self, lon, lat, h_cc, v_cc):
        return self.__mem_cache_map.get(lon, lat, h_cc, v_cc)

    def get_weather_data(self, lon, lat, h_cc, v_cc):
        return self.__mem_cache_weather.get(lon, lat, h_cc, v_cc)


if __name__ == "__main__":
    c = LocalCache(r"D:\source\python\navigation-plan-engine\postgres2osm\test\config\navieng.json")
    c.server_setup_map_cache()
    c.read_map_cache()
    c.read_weather_cache()
    # print(LocalCache.__next_id("D:\\temp\\map0011.npy"))
    # print(LocalCache.__next_id("D:\\temp\\map0030a1.npy"))
    # print(LocalCache.__next_id("D:\\temp\\map003001.dat"))
    # print(LocalCache.__next_id("D:\\temp\\map003001.dat0001.npy"))